#!/bin/sh
#*********> Script to Attach MCP Update Log To Multiple Executables <***********
#*********> ======================================================= <***********

# This tool is a wrapper around the tools to mangage an individual update
# log. This tool is meant to maintain a set of update logs on a single image.
# These tools can be used to add an entry to all update logs, synchronize all
# update logs, or verify the integrity of all update logs.

# Usage:
#
#	sync_log_update update_distro_id update_pkg_name [exe_filename...]
#	check_update_log [exe_filename...]
#	check_distro_id [exe_filename...]

# Revision ID: $Id$

#*******************************************************************************
#*******************************************************************************

# SCRATCH_DIR - Where we keep the herd of tmp files that we create.

# ExeList - List of ELF executables that might have a distro ID or update log.
# MustFindAll - Flag indicating how serious we are about finding all files in
#	the ExeList.


SCRATCH_DIR="${TMP_DIR:-/tmp}/sync_log_update$$"
TRUE=0
FALSE=1

ExeList="xinit4 bash fsck busybox dhcpcd"
MustFindAll=$FALSE






##
## Usage Message for check_update_log(1).
##

CheckUpdateLogUsage () {
	{
	echo "Usage:"
	echo "	check_update_log [exe_filename...]"
	} >&2
}


##
## Usage Message for check_distro_id(1).
##

CheckDistroIdUsage () {
	{
	echo "Usage:"
	echo "	check_distro_id [exe_filename...]"
	} >&2
}


##
## Usage Message for check_distro_id(1).
##

SyncLogUpdateUsage () {
	{
	echo "Usage:"
	echo "	sync_log_update update_distro_id update_package_name [exe_filename...]"
	} >&2
}



##
## To Full Paths Routine
##

# The ExeList needs to be converted to full paths.

Exe2FullPath () {

	Rtn=$TRUE

	# Use the default path, and add std locations for places to look for
	# executables.

	SearchPath="`echo $PATH | sed -e 's,:, ,g'`"
	SearchPath="$SearchPath /bin /sbin /usr/bin /usr/sbin /usr/X11R6/bin /usr/local/sbin /usr/local/bin ."

	NewExeList=""
	for Exe in $ExeList; do

		# Skip anything with a full path already.

		FirstChar=`expr $Exe : '\(.\).*'`
		Bingo=$FALSE
		if [  $FirstChar = "/" ]; then
			NewExeList="$NewExeList $Exe"
			continue
		fi

		# No full path, we'll have to try and find it.

		for Dir in $SearchPath; do
			if [ -f "$Dir/$Exe" ]; then
				NewExeList="$NewExeList $Dir/$Exe"
				Bingo=$TRUE
				break
			fi
		done

		# If we get here, we don't know what the path to the executable is.

		if [ "$Bingo" -ne $TRUE ]; then
			if [ -n "$WARN" ] || [ "$Bingo" -ne $TRUE ]; then
				echo "$Prog: Cannot find path to file ($Exe). Update \$PATH." >&2
			fi
			if [ "$MustFindAll" -eq $TRUE ]; then
				Rtn=$FALSE
			fi
		fi
	done

	ExeList="$NewExeList"

	return $Rtn

} #Exe2FullPath



##
## Compare Two Files
##

# Why does this routine exist? We need a way to compare without the diffutils
# package. So, we use the shell to check if two lines match.

CmpFiles () {
	# $1 - First file to compare.
	# $2 - Second file to compare.

	# Do I/O redirection

	exec 3< $1
	exec 4< $2

	# Use shell to compare lines of the files

	Match=$TRUE
	while read First <&3; do
		if read Second <&4; then
			if [ "$First" != "$Second" ]; then
				Match=$FALSE
				break
			fi
		else
			Match=$FALSE
			break
		fi
	done

	# Make sure second file doesn't have any extra lines.

	if [ "$Match" -eq 0 ]; then
		if read Second <&4; then
			Match=$FALSE
		fi
	fi

	exec 3<&- #close file $1
	exec 4<&- #close file $2

	return $Match

} #CmpFiles


##
## Strip Suffix routine
##

StripSuffix () {
	# $1 - Name to strip suffix from.
	# $2 - Suffix

	echo "$1" | sed -e s,$2\$,,

	return $TRUE

} #StripSuffix


##
## Dirname routine
##

DirName () {
	# $1 - String to extract dirname from

	echo $1 | sed -e s,\\\(.*\\\)/[^/]*,\\1,

	return 0
} #DirName



##
## Basename routine
##

BaseName () {
	# $1 - String to extract basename from
	# $2 - Optional, suffix to strip

	{
	if [ -n "$2" ]; then
		echo `StripSuffix $1 $2`
	else
		echo $1
	fi
	} | sed -e s,.*/\\\([^/]*\\\),\\1,

	return $TRUE
} #BaseName


##
## Create Minimum Update Log routine
##

# This routine creates a fixed minimum size update log with just the
# lines containing the distro ID and package name.

CreateMinLog () {
	# $1 - Name of raw update log file.

	Rtn=$TRUE

	Stem="`BaseName $1 .log`"
	# Filter out all lines that don't begin with a '/number/' format string.
	# Trim off starting with 'update on'.
	# Trim off format version number.
	# Delete blank lines
	sed -e '/^\/[0-9][0-9]*\//!s,.*,,' \
	  -e 's,[ \t]*updated on.*,,' \
	  -e 's,^\/[0-9][0-9]*\/[^ \t],,' \
	  -e '/^[ \t]*$/d' \
	  $1 > $SCRATCH_DIR/$Stem.minlog

	# We need to check for unrecognized formats, and adjust to
	# a common format. The sed search here can be expanded for new
	# versions. Normally, we'd just re-write the lines to a common
	# format.

	sed -e '/^\/1\//!s,.*,,' $SCRATCH_DIR/$Stem.minlog \
	  > $SCRATCH_DIR/$Stem.minlogx
	if ! CmpFiles $SCRATCH_DIR/$Stem.minlogx $SCRATCH_DIR/$Stem.minlog; then
		echo "$Prog: Unrecognized update log format for ($Stem). Please use newer version of $Prog." >&2
		Rtn=$FALSE
	fi

	# Strip down to our common format of just the distro ID and package name.

	sed -e '/^\/1\//s,^[ \t]*/1/[ \t]*,,' $SCRATCH_DIR/$Stem.minlogx \
	  > $SCRATCH_DIR/$Stem.minlog

	rm -f $SCRATCH_DIR/$Stem.minlogx

	return $Rtn

} #CreateMinLog



##
## Create Delta Of MinLog file.
##

# Minimum logs may not be the same. One may be a subset of the other. If so
# a delta file needs to be created with the missing additional entries. Delta
# files are created if one file is a subset of the other. The original file
# plus the delta file would match the superset file. The delta file is the
# original filename with '.delta' appended. The calling routine is responsible
# for doing something useful with the delta file. Previous versions of delta
# files will be silently overwritten.
#
# There are 4 possible outcomes.
#
# $1 == $2 No delta files. Return 0 (TRUE).
# $1 is subset of $2. Return 1 ($1 will have a delta file).
# $2 is subset of $1. Return 2 ($2 will have a delta file).
# $1 is subset of $2. Are just different. This is a problem requiring human
#	intervention (since a human probably caused it... it shouldn't happen
#	except by human screwup).

DeltaMinLog () {
	# $1 - First file to compare.
	# $2 - Second file to compare.

	# Do I/O redirection

	exec 3< $1
	exec 4< $2

	# Use shell to compare lines of the files

	Rtn=0
	LclDeltaFile=""
	while read First <&3; do
		if read Second <&4; then
			if [ "$First" != "$Second" ]; then
				Rtn=255
				break
			fi
		else
			# We have a line from $1, but not from $2. $2 will need a delta
			# file. Perhaps we've already done this once?

			if [ -z "$LclDeltaFile" ]; then
				Rtn=2
				LclDeltaFile="$2.delta"
				rm -f $LclDeltaFile
			fi
			echo "$First" >> $LclDeltaFile
		fi
	done

	# Make sure second file doesn't have any extra lines.

	if [ "$Rtn" -eq 0 ]; then
		while read Second <&4; do
			# We have a line from $2, but not from $1. $1 will need a delta
			# file. Perhaps we've already done this once?

			if [ -z "$LclDeltaFile" ]; then
				Rtn=1
				LclDeltaFile="$1.delta"
				rm -f $LclDeltaFile
			fi
			echo "$Second" >> $LclDeltaFile
		done
	fi

	exec 3<&- #close file $1
	exec 4<&- #close file $2

	return $Rtn

} #DeltaMinLog



##
## Lookup Distro ID By Update Package Name
##

# This routine extracts the distro ID matching a particular package from the
# update log.


GetDistroIDByUpdate () {
	# $1 - name of package to match
	# $2 - name of executable to check

	GD_TmpFile="$SCRATCH_DIR/`BaseName $2`.log"
	if ! update_log $Exe > $GD_TmpFile 2> $SCRATCH_DIR/update_log.log; then
		if [ -n "$WARN" ]; then
			cat $SCRATCH_DIR/update_log.log >&2
			echo "$Prog: Cannot get update log from ($2)." >&2
			if [ "$MustFindAll" -eq $TRUE ]; then
				return $FALSE
			fi
		fi
	fi

	if ! CreateMinLog $GD_TmpFile; then
		echo "$Prog: Failed to create minlog for ($2)." >&2
		return $FALSE
	fi

	Stem="`BaseName $2`"
	GD_RegExpr="\(.*\) $1"
	while read $Line; do
		Part1="`expr \"$Line\" : \"$GD_RegExpr\"`"
		if [ -n "$Part1" ]; then
			echo "$Part1"
			return $TRUE
		fi
	done < $SCRATCH_DIR/$Stem.minlog

	return $FALSE

} #GetDistroIDByUpdate


##
## Cleanup routine
##

CleanUp () {
	rm -fr $SCRATCH_DIR
}



##
## Create Canonical Log
##

# This routine goes through the $SCRATCH_DIR/*.minlog and creates a
# log that contains all update log entries. The canonical log is used as
# a reference.  All logs should match it. If an routine error occurred,
# then some logs may be a subset... and need to be updated. If there are
# logs that aren't a subset someone has been messing with the logs and
# manual rectification is required.

CreateCanonicalLog () {

	CanonRtn=$TRUE

	# Pull the update logs.

	for Exe in $ExeList; do
		TmpFile="$SCRATCH_DIR/`BaseName $Exe`.log"
		if ! update_log $Exe > $TmpFile 2> $SCRATCH_DIR/update_log.log; then
			if [ -n "$WARN" ]; then
				cat $SCRATCH_DIR/update_log.log >&2
				echo "$Prog: Cannot get update log from ($Exe)." >&2
				if [ "$MustFindAll" -eq $TRUE ]; then
					exit 1
				fi
			fi
		fi
	done

	# Create mininum logs. They also have 'format version', 'updated
	# by' and 'time' that we don't care about. Scraping these out allows
	# the logs to be diffed.

	for Log in `ls $SCRATCH_DIR/*.log`; do
		CreateMinLog $Log
	done

	# Create a canonical log by comparing each different update log.

	for MLog in `ls $SCRATCH_DIR/*.minlog`; do

		# First log is the initial canonical log.

		if [ ! -f "$SCRATCH_DIR/canonical.mlog" ]; then
			cp $MLog $SCRATCH_DIR/canonical.mlog
			continue
		fi

		# Create a canonical log that is a superset of all
		# minlogs. Order of entries counts.

		DeltaMinLog $SCRATCH_DIR/canonical.mlog $MLog
		Tmp=$?
		case $Tmp in
		0) : ;; #do nothing logs matched
		1) #Canonical log needs to be longer
			cat $MLog.delta >> $SCRATCH_DIR/canonical.mlog
			;;
		2) #Minlog needs to be longer
			: #do nothing
			;;
		*) #Things are foobar. A human will have to fix.
			MLogStem="`BaseName $MLog .minlog`"
			echo "$Prog: Update log for ($MLogStem) is not a subset of the most complete update log available. Manual synchronization of the udpate logs is required." >&2
			CanonRtn=$FALSE
			break
			;;
		esac
	done

	return $CanonRtn

} #CreateCanonicalLog



##
## Main
##

# Extract the program name from the command line.

Prog=`expr /$0 : '.*/\(.*\)'`
trap "CleanUp; exit 2" 1 2 3 15

# This script is really several programs in one. Which version is the user
# trying to invoke (get from name of command).

if ! mkdir -p $SCRATCH_DIR; then
	echo "$Prog: Failed to make scratch directory ($SCRATCH_DIR)." >&2
	exit 1
fi

# Strictly speaking the next line isn't required. However, when debugging
# it helps a whole lot... so leave it here.

rm -f $SCRATCH_DIR/*

ExitCode=$TRUE
if [ "$Prog" = "check_update_log" ]; then

	# Make sure all args look like file names.

	FileList=""
	while [ $# -gt 0 ]; do
		case "$1" in
		-*)
			echo "$Prog: option ($1) not recognized." >&2
			CheckUpdateLogUsage
			exit 1
			;;
			
		*) FileList="$FileList $1";;
		esac
		shift
	done

	# If we're guessing the executables with a distro ID or update log, then
	# we could be wrong... so be quiet about problems. On the other hand if
	# we're told explictly what to find, be harsh we must find it.

	MustFindAll=$FALSE
	if [ -n "$FileList" ]; then
		ExeList="$FileList"
		MustFindAll=$TRUE
	fi

	# Convert file names to full path names. We do a search because files
	# can be in different locations in different MCP loads.

	if ! Exe2FullPath; then
		echo "$Prog: Failed finding full path name of executables with update log." >&2
		exit 1
	fi

	# Pull and compare update logs.

	for Exe in $ExeList; do
		TmpFile="$SCRATCH_DIR/`BaseName $Exe`.log"
		if ! update_log $Exe > $TmpFile 2> $SCRATCH_DIR/update_log.log; then
			if [ -n "$WARN" ]; then
				cat $SCRATCH_DIR/update_log.log >&2
				echo "$Prog: Cannot get update log from ($Exe)." >&2
				if [ "$MustFindAll" -eq $TRUE ]; then
					exit 1
				fi
			fi
		fi
	done

	# Create mininum logs. They also have 'updated by' and 'time' that we don't
	# care about. Scraping these out allows the logs to be diffed.

	for Log in `ls $SCRATCH_DIR/*.log`; do
		CreateMinLog $Log
	done

	ReferenceLog=""
	for Exe in $ExeList; do
		MLog="$SCRATCH_DIR/`BaseName $Exe`.minlog"
		if [ ! -f "$MLog" ]; then
			continue
		fi

		# First log is the reference log.

		if [ -z "$ReferenceLog" ]; then
			ReferenceLog="$MLog"
			RefExe="$Exe"
			continue
		fi

		if ! CmpFiles $ReferenceLog $MLog; then
			RefStem="`BaseName $ReferenceLog .minlog`"
			MLogStem="`BaseName $MLog .minlog`"
			echo "$Prog: Update logs for ($RefExe) and ($Exe) do not match." >&2
			ExitCode=$FALSE;
		fi
	done

elif [ "$Prog" = "check_distro_id" ]; then

	# Make sure all args look like file names.

	FileList=""
	while [ $# -gt 0 ]; do
		case "$1" in
		-*)
			echo "$Prog: option ($1) not recognized." >&2
			CheckDistroIdUsage
			exit 1
			;;
			
		*) FileList="$FileList $1";;
		esac
		shift
	done

	# If we're guessing the executables with a distro ID or update log, then
	# we could be wrong... so be quiet about problems. On the other hand if
	# we're told explictly what to find, be harsh we must find it.

	MustFindAll=$FALSE
	if [ -n "$FileList" ]; then
		ExeList="$FileList"
		MustFindAll=$TRUE
	fi

	# Convert file names to full path names. We do a search because files
	# can be in different locations in different MCP loads.

	if ! Exe2FullPath; then
		echo "$Prog: Failed finding full path name of executables with update log." >&2
		exit 1
	fi

	# Pull and compare distro IDs.

	for Exe in $ExeList; do
		TmpFile="$SCRATCH_DIR/`BaseName $Exe`.id"
		if ! distro_id $Exe > $TmpFile; then
			if [ -n "$WARN" ]; then
				echo "$Prog: Cannot get update distribution ID from ($Exe)." >&2
				if [ "$MustFindAll" -eq $TRUE ]; then
					exit 1
				fi
			fi
			ExitCode=$FALSE
		fi
	done

	# Create mininum logs. They also have 'updated by' and 'time' that we don't
	# care about. Scraping these out allows the logs to be diffed.

	RefIs=""
	ReferenceId=""
	if [ -f /proc/distro_id ]; then
		RefIs=/proc/distro_id
		ReferenceId="`cat /proc/distro_id 2> /dev/null`"
	fi
	for IdFile in `ls $SCRATCH_DIR/*.id`; do

		# First distro ID is the reference ID, if we didn't have
		# a /proc/distro_id.

		if [ -z "$ReferenceId" ]; then
			ReferenceId="`cat $IdFile`"
			RefIs="`BaseName $IdFile .id`"
			continue
		fi

		Id="`cat $IdFile`"
		if [ "$ReferenceId" !=  "$Id" ]; then
			IdStem="`BaseName $IdFile .id`"
			echo "$Prog: Distribution IDs for ($RefIs) and ($IdStem) do not match." >&2
			ExitCode=$FALSE;
		fi
	done

elif [ "$Prog" = "sync_log_update" ]; then

	# We may have a distribution ID and an update package name.

	DistroId=""
	UpdatePkg=""
	FileList=""
	if [ $# -ge 2 ]; then
		# It's easy to recognize the distro ID and the package name. Check
		# that the distro ID looks like a bunch of colon separated fields.

    	Match="`expr \"$1\" : '[0-9]:.*:.*:'`" #0 means no match 
    	if [ "$Match" -ne 0 ]; then 
			DistroId="$1"
			UpdatePkg="$2"
			shift; shift
    	fi 
	fi

	# Make sure all remaining args look like file names.

	while [ $# -gt 0 ]; do
		case "$1" in
		-*)
			echo "$Prog: option ($1) not recognized." >&2
			SyncLogUpdateUsage
			exit 1
			;;
			
		*) FileList="$FileList $1";;
		esac
		shift
	done

	# If we're guessing the executables with a distro ID or update log, then
	# we could be wrong... so be quiet about problems. On the other hand if
	# we're told explictly what to find, be harsh we must find it.

	MustFindAll=$FALSE
	if [ -n "$FileList" ]; then
		ExeList="$FileList"
		MustFindAll=$TRUE
	fi

	# Convert file names to full path names. We do a search because files
	# can be in different locations in different MCP loads.

	if ! Exe2FullPath; then
		echo "$Prog: Failed finding full path name of executables with update log." >&2
		CleanUp
		exit $FALSE
	fi

	# Create a canonical update log for the system. This can then be
	# used to make sure all other logs are up to date.

	if ! CreateCanonicalLog; then
		echo "$Prog: Creation of canonical update log failed." >&2
		CleanUp
		exit $FALSE
	fi

	# We've got a canonical log. Create the needed delta logs for
	# each exe.

	rm -f $SCRATCH_DIR/*.delta
	for Exe in $ExeList; do
		MLog="$SCRATCH_DIR/`BaseName $Exe`.minlog"
		if [ ! -f "$MLog" ]; then
			continue
		fi

		DeltaMinLog $SCRATCH_DIR/canonical.mlog $MLog
		Tmp=$?
		case $Tmp in
		0) : ;; #do nothing logs matched
		1) #Canonical log needs to be longer, this shouldn't happen.
			echo "$Prog: Somehow update log for ($Exe) has grown. Manual synchronization of the udpate logs is required." >&2
			CleanUp
			exit $FALSE
			;;
		2) # We need to append to the update log of the current exe.

			while read Line; do
				echo "$Prog: log_update $Line $Exe"
				if ! log_update $Line $Exe; then
					echo "$Prog: log_update failed for ($Exe)." >&2
					CleanUp
					exit $FALSE
				fi
			done < $MLog.delta
			;;
		*) #Things are foobar. A human will have to fix.
			echo "$Prog: Update log for ($Exe) is not a subset of the most complete update log available. Manual synchronization of the udpate logs is required." >&2
			CleanUp
			exit $FALSE
			;;
		esac
	done

	# If we get this far, then all the update logs are intact. If the current
	# update log entry isn't present, add it.

	if [ -n "$DistroId" ] && [ -n "$UpdatePkg" ]; then
		for Exe in $ExeList; do
			# Check for duplicate entry in update log. If found we report an
			# error, don't add it to the log, and keep going.

			ExeErr=$FALSE
			PkgId="`GetDistroIDByUpdate $UpdatePkg $Exe`"
			if [ -n "$PkgId" ]; then
				if [ "$PkgId" != "$DistroId" ]; then
					echo "$Prog: Update ($PkgId) has already been added to the update log ($Exe), but has a different distribution ID ($PkgId)." >&2
					ExeErr=$TRUE
					ExitCode=1
				fi
			fi

			if [ $ExeErr -eq $FALSE ]; then
				if ! log_update $DistroId $UpdatePkg $Exe; then
					echo "$Prog: log_update failed for ($Exe)." >&2
					CleanUp
					exit $FALSE
				fi
			fi
		done
	fi
fi

CleanUp

exit $ExitCode
